msg_tool\scripts\yaneurao\itufuru/
archive.rs1use super::crypto::*;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::encoding::encode_string;
7use crate::utils::struct_pack::*;
8use anyhow::Result;
9use msg_tool_macro::*;
10use std::collections::HashMap;
11use std::io::{Read, Seek, SeekFrom, Write};
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug)]
15pub struct ItufuruArchiveBuilder {}
17
18impl ItufuruArchiveBuilder {
19 pub const fn new() -> Self {
21 ItufuruArchiveBuilder {}
22 }
23}
24
25impl ScriptBuilder for ItufuruArchiveBuilder {
26 fn default_encoding(&self) -> Encoding {
27 Encoding::Cp932
28 }
29
30 fn default_archive_encoding(&self) -> Option<Encoding> {
31 Some(Encoding::Cp932)
32 }
33
34 fn build_script(
35 &self,
36 data: Vec<u8>,
37 _filename: &str,
38 _encoding: Encoding,
39 archive_encoding: Encoding,
40 config: &ExtraConfig,
41 _archive: Option<&Box<dyn Script>>,
42 ) -> Result<Box<dyn Script>> {
43 Ok(Box::new(ItufuruArchive::new(
44 MemReader::new(data),
45 archive_encoding,
46 config,
47 )?))
48 }
49
50 fn build_script_from_file(
51 &self,
52 filename: &str,
53 _encoding: Encoding,
54 archive_encoding: Encoding,
55 config: &ExtraConfig,
56 _archive: Option<&Box<dyn Script>>,
57 ) -> Result<Box<dyn Script>> {
58 if filename == "-" {
59 let data = crate::utils::files::read_file(filename)?;
60 Ok(Box::new(ItufuruArchive::new(
61 MemReader::new(data),
62 archive_encoding,
63 config,
64 )?))
65 } else {
66 let f = std::fs::File::open(filename)?;
67 let reader = std::io::BufReader::new(f);
68 Ok(Box::new(ItufuruArchive::new(
69 reader,
70 archive_encoding,
71 config,
72 )?))
73 }
74 }
75
76 fn build_script_from_reader(
77 &self,
78 reader: Box<dyn ReadSeek>,
79 _filename: &str,
80 _encoding: Encoding,
81 archive_encoding: Encoding,
82 config: &ExtraConfig,
83 _archive: Option<&Box<dyn Script>>,
84 ) -> Result<Box<dyn Script>> {
85 Ok(Box::new(ItufuruArchive::new(
86 reader,
87 archive_encoding,
88 config,
89 )?))
90 }
91
92 fn extensions(&self) -> &'static [&'static str] {
93 &["scd"]
94 }
95
96 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
97 if buf_len >= 4 && buf.starts_with(b"SCR\0") {
98 Some(1)
99 } else {
100 None
101 }
102 }
103
104 fn script_type(&self) -> &'static ScriptType {
105 &ScriptType::YaneuraoItufuruArc
106 }
107
108 fn is_archive(&self) -> bool {
109 true
110 }
111
112 fn create_archive(
113 &self,
114 filename: &str,
115 files: &[&str],
116 encoding: Encoding,
117 config: &ExtraConfig,
118 ) -> Result<Box<dyn Archive>> {
119 let f = std::fs::File::create(filename)?;
120 let writer = std::io::BufWriter::new(f);
121 let archive = ItufuruArchiveWriter::new(writer, files, encoding, config)?;
122 Ok(Box::new(archive))
123 }
124}
125
126#[derive(Debug, StructPack, StructUnpack)]
127struct ItufuruFileHeader {
128 #[fstring = 12]
129 file_name: String,
130 offset: u32,
131}
132
133#[derive(Debug, StructPack)]
134struct CustomHeader {
135 #[fstring = 12]
136 file_name: String,
137 offset: u32,
138 #[skip_pack]
139 size: u32,
140}
141
142struct Entry {
143 name: String,
144 data: MemReader,
145}
146
147impl Read for Entry {
148 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
149 self.data.read(buf)
150 }
151}
152
153impl ArchiveContent for Entry {
154 fn name(&self) -> &str {
155 &self.name
156 }
157
158 fn script_type(&self) -> Option<&ScriptType> {
159 Some(&ScriptType::YaneuraoItufuru)
160 }
161
162 fn data(&mut self) -> Result<Vec<u8>> {
163 Ok(self.data.data.clone())
164 }
165
166 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
167 Ok(Box::new(&mut self.data))
168 }
169}
170
171#[derive(Debug)]
172pub struct ItufuruArchive<T: Read + Seek + std::fmt::Debug> {
174 reader: Arc<Mutex<Crypto<T>>>,
175 first_file_offset: u32,
176 files: Vec<CustomHeader>,
177}
178
179impl<T: Read + Seek + std::fmt::Debug> ItufuruArchive<T> {
180 pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
186 let mut header = [0u8; 4];
187 reader.read_exact(&mut header)?;
188 if &header != b"SCR\0" {
189 return Err(anyhow::anyhow!("Invalid Itufuru archive header"));
190 }
191 let file_count = reader.read_u32()?;
192 let first_file_offset = reader.read_u32()?;
193 reader.read_u32()?; let mut reader = Crypto::new(reader, 0xA5);
195 let mut tfiles = Vec::with_capacity(file_count as usize);
196 for _ in 0..file_count {
197 let file = ItufuruFileHeader::unpack(&mut reader, false, archive_encoding)?;
198 tfiles.push(file);
199 }
200 let mut files = Vec::with_capacity(tfiles.len());
201 if !tfiles.is_empty() {
202 for i in 0..tfiles.len() - 1 {
203 let file = CustomHeader {
204 file_name: tfiles[i].file_name.clone(),
205 offset: tfiles[i].offset,
206 size: tfiles[i + 1].offset - tfiles[i].offset,
207 };
208 files.push(file);
209 }
210 let last_file = &tfiles[tfiles.len() - 1];
211 let file = CustomHeader {
212 file_name: last_file.file_name.clone(),
213 offset: last_file.offset,
214 size: reader.seek(SeekFrom::End(0))? as u32 - last_file.offset - first_file_offset,
215 };
216 files.push(file);
217 }
218 Ok(ItufuruArchive {
219 reader: Arc::new(Mutex::new(reader)),
220 first_file_offset,
221 files,
222 })
223 }
224}
225
226impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for ItufuruArchive<T> {
227 fn default_output_script_type(&self) -> OutputScriptType {
228 OutputScriptType::Json
229 }
230
231 fn default_format_type(&self) -> FormatOptions {
232 FormatOptions::None
233 }
234
235 fn is_archive(&self) -> bool {
236 true
237 }
238
239 fn iter_archive_filename<'a>(
240 &'a self,
241 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
242 Ok(Box::new(
243 self.files.iter().map(|s| Ok(s.file_name.to_owned())),
244 ))
245 }
246
247 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
248 Ok(Box::new(self.files.iter().map(|s| Ok(s.offset as u64))))
249 }
250
251 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
252 if index >= self.files.len() {
253 return Err(anyhow::anyhow!(
254 "Index out of bounds: {} (max: {})",
255 index,
256 self.files.len()
257 ));
258 }
259 let entry = &self.files[index];
260 let file_offset = entry.offset as u64;
261 match self.reader.cpeek_exact_at_vec(
262 file_offset + self.first_file_offset as u64,
263 entry.size as usize,
264 ) {
265 Ok(data) => {
266 let name = entry.file_name.clone();
267 Ok(Box::new(Entry {
268 name,
269 data: MemReader::new(data),
270 }))
271 }
272 Err(e) => Err(anyhow::anyhow!(
273 "Failed to read file {}: {}",
274 entry.file_name,
275 e
276 )),
277 }
278 }
279}
280
281pub struct ItufuruArchiveWriter<T: Write + Seek> {
283 writer: T,
284 headers: HashMap<String, CustomHeader>,
285 first_file_offset: u32,
286 encoding: Encoding,
287}
288
289impl<T: Write + Seek> ItufuruArchiveWriter<T> {
290 pub fn new(
297 mut writer: T,
298 files: &[&str],
299 encoding: Encoding,
300 _config: &ExtraConfig,
301 ) -> Result<Self> {
302 writer.write_all(b"SCR\0")?;
303 let file_count = files.len() as u32;
304 writer.write_u32(file_count)?;
305 let first_file_offset = 0x10 + file_count * 16; writer.write_u32(first_file_offset)?;
307 writer.write_u32(0)?; let mut headers = HashMap::new();
309 for file in files {
310 headers.insert(
311 file.to_string(),
312 CustomHeader {
313 file_name: file.to_string(),
314 offset: 0,
315 size: 0,
316 },
317 );
318 }
319 let mut crypto = Crypto::new(&mut writer, 0xA5);
320 for (_, header) in headers.iter() {
321 header.pack(&mut crypto, false, encoding)?;
322 }
323 Ok(ItufuruArchiveWriter {
324 writer,
325 headers,
326 first_file_offset,
327 encoding,
328 })
329 }
330}
331
332impl<T: Write + Seek> Archive for ItufuruArchiveWriter<T> {
333 fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
334 let entry = self
335 .headers
336 .get_mut(name)
337 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
338 if entry.size != 0 {
339 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
340 }
341 entry.offset = self.writer.stream_position()? as u32 - self.first_file_offset;
342 Ok(Box::new(ItufuruArchiveWriterEntry::new(
343 &mut self.writer,
344 entry,
345 self.first_file_offset,
346 )))
347 }
348 fn write_header(&mut self) -> Result<()> {
349 let mut crypto = Crypto::new(&mut self.writer, 0xA5);
350 let mut entries = self.headers.values().collect::<Vec<_>>();
351 entries.sort_by_key(|h| h.offset);
352 crypto.seek(SeekFrom::Start(16))?;
353 for entry in entries.iter() {
354 entry.pack(&mut crypto, false, self.encoding)?;
355 }
356 Ok(())
357 }
358}
359
360pub struct ItufuruArchiveWriterEntry<'a, T: Write + Seek> {
362 writer: Crypto<&'a mut T>,
363 header: &'a mut CustomHeader,
364 first_file_offset: u32,
365 pos: usize,
366}
367
368impl<'a, T: Write + Seek> ItufuruArchiveWriterEntry<'a, T> {
369 fn new(writer: &'a mut T, header: &'a mut CustomHeader, first_file_offset: u32) -> Self {
370 let writer = Crypto::new(writer, 0xA5);
371 ItufuruArchiveWriterEntry {
372 writer,
373 header,
374 first_file_offset,
375 pos: 0,
376 }
377 }
378}
379
380impl<'a, T: Write + Seek> Write for ItufuruArchiveWriterEntry<'a, T> {
381 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
382 self.writer.seek(SeekFrom::Start(
383 self.header.offset as u64 + self.first_file_offset as u64 + self.pos as u64,
384 ))?;
385 let written = self.writer.write(buf)?;
386 self.pos += written;
387 self.header.size = self.header.size.max(self.pos as u32);
388 Ok(written)
389 }
390
391 fn flush(&mut self) -> std::io::Result<()> {
392 self.writer.flush()
393 }
394}
395
396impl<'a, T: Write + Seek> Seek for ItufuruArchiveWriterEntry<'a, T> {
397 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
398 let new_pos = match pos {
399 SeekFrom::Start(offset) => offset as usize,
400 SeekFrom::End(offset) => {
401 if offset < 0 {
402 if (-offset) as usize > self.header.size as usize {
403 return Err(std::io::Error::new(
404 std::io::ErrorKind::InvalidInput,
405 "Seek from end exceeds file length",
406 ));
407 }
408 self.header.size as usize - (-offset) as usize
409 } else {
410 self.header.size as usize + offset as usize
411 }
412 }
413 SeekFrom::Current(offset) => {
414 if offset < 0 {
415 if (-offset) as usize > self.pos {
416 return Err(std::io::Error::new(
417 std::io::ErrorKind::InvalidInput,
418 "Seek from current exceeds current position",
419 ));
420 }
421 self.pos.saturating_sub((-offset) as usize)
422 } else {
423 self.pos + offset as usize
424 }
425 }
426 };
427 self.pos = new_pos;
428 Ok(self.pos as u64)
429 }
430
431 fn stream_position(&mut self) -> std::io::Result<u64> {
432 Ok(self.pos as u64)
433 }
434}